{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Testing with [pytest](https://docs.pytest.org/en/latest/) - part 2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Let's make sure pytest and ipytest packages are installed\n",
    "# ipytest is required for running pytest inside Jupyter notebooks\n",
    "import sys\n",
    "!{sys.executable} -m pip install pytest\n",
    "!{sys.executable} -m pip install ipytest\n",
    "\n",
    "import ipytest.magics\n",
    "import pytest\n",
    "__file__ = 'testing2.ipynb'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## [`@pytest.fixture`](https://docs.pytest.org/en/latest/fixture.html#pytest-fixtures-explicit-modular-scalable)\n",
    "Let's consider we have an implemention of `Person` class which we want to test."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# This would be e.g. in person.py\n",
    "class Person:\n",
    "    def __init__(self, first_name, last_name, age):\n",
    "        self.first_name = first_name\n",
    "        self.last_name = last_name\n",
    "        self.age = age\n",
    "    \n",
    "    @property\n",
    "    def full_name(self):\n",
    "        return '{} {}'.format(self.first_name, self.last_name)\n",
    "    \n",
    "    @property\n",
    "    def as_dict(self):\n",
    "        return {'name': self.full_name, 'age': self.age}\n",
    "        \n",
    "    def increase_age(self, years):\n",
    "        if years < 0:\n",
    "            raise ValueError('Can not make people younger :(')\n",
    "        self.age += years"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can easily create resusable testing code by using pytest fixtures. If you introduce your fixtures inside [_conftest.py_](https://docs.pytest.org/en/latest/fixture.html#conftest-py-sharing-fixture-functions), the fixtures are available for all your test cases. In general, the location of _conftest.py_ is at the root of your _tests_ directory."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# This would be in either conftest.py or test_person.py\n",
    "@pytest.fixture()\n",
    "def default_person():\n",
    "    person = Person(first_name='John', last_name='Doe', age=82)\n",
    "    return person"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then you can utilize `default_person` fixture in the actual test cases. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%run_pytest[clean]\n",
    "\n",
    "# These would be in test_person.py\n",
    "def test_full_name(default_person): # Note: we use fixture as an argument of the test case\n",
    "    result = default_person.full_name\n",
    "    assert result == 'John Doe'\n",
    "    \n",
    "    \n",
    "def test_as_dict(default_person):\n",
    "    expected = {'name': 'John Doe', 'age': 82}\n",
    "    result = default_person.as_dict\n",
    "    assert result == expected\n",
    "    \n",
    "    \n",
    "def test_increase_age(default_person):\n",
    "    default_person.increase_age(1)\n",
    "    assert default_person.age == 83\n",
    "    \n",
    "    default_person.increase_age(10)\n",
    "    assert default_person.age == 93\n",
    "    \n",
    "    \n",
    "def test_increase_age_with_negative_number(default_person):\n",
    "    with pytest.raises(ValueError):\n",
    "        default_person.increase_age(-1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "By using a fixture, we could use the same `default_person` for all our four test cases!\n",
    "\n",
    "In the `test_increase_age_with_negative_number` we used [`pytest.raises`](https://docs.pytest.org/en/latest/assert.html#assertions-about-expected-exceptions) to verify that an exception is raised. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## [`@pytest.mark.parametrize`](https://docs.pytest.org/en/latest/parametrize.html#pytest-mark-parametrize-parametrizing-test-functions)\n",
    "Sometimes you want to test the same functionality with multiple different inputs. `pytest.mark.parametrize` is your solution for defining multiple different inputs with expected outputs. Let's consider the following implementation of `replace_names` function. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# This would be e.g. in string_manipulate.py\n",
    "def replace_names(original_str, new_name):\n",
    "    \"\"\"Replaces names (uppercase words) of original_str by new_name\"\"\"\n",
    "    words = original_str.split()\n",
    "    manipulated_words = [new_name if w.istitle() else w for w in words]\n",
    "    return ' '.join(manipulated_words)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can test the `replace_names` function with multiple inputs by using `pytest.mark.parametrize`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%run_pytest[clean]\n",
    "\n",
    "# This would be in your test module\n",
    "@pytest.mark.parametrize(\"original,new_name,expected\", [\n",
    "        ('this is Lisa', 'John Doe', 'this is John Doe'),\n",
    "        ('how about Frank and Amy', 'John', 'how about John and John'),\n",
    "        ('no names here', 'John Doe', 'no names here'),\n",
    "    ])\n",
    "def test_replace_names(original, new_name, expected):\n",
    "    result = replace_names(original, new_name)\n",
    "    assert result == expected"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
